package jp.dodododo.aop.interceptors;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import jp.dodododo.aop.annotation.Enhance;

import org.aopalliance.intercept.MethodInvocation;

/**
 * {@.en }
 * 
 * <br />
 * 
 * {@.ja Cache用のInterceptorです。}
 * 
 * @author Satoshi Kimura
 */
@Enhance(false)
public class CacheInterceptor extends AbstractMethodInterceptor {

	private static Map<String, CacheInterceptor> cacheInterceptors = new HashMap<String, CacheInterceptor>();

	private static Map<CallDesc, CacheData> cacheMap = new HashMap<CallDesc, CacheData>();

	private long expirationMilliseconds;

	protected CacheInterceptor(long expirationMilliseconds) {
		this.expirationMilliseconds = expirationMilliseconds;
	}

	public static CacheInterceptor getInstance() {
		return new CacheInterceptor(-1);
	}

	public static CacheInterceptor getInstance(String namespace) {
		return getInstance(namespace, -1);
	}

	public static CacheInterceptor getInstance(int expirationMilliseconds) {
		return getInstance((long) expirationMilliseconds);
	}

	public static CacheInterceptor getInstance(long expirationMilliseconds) {
		String namespace = CacheInterceptor.class.getName() + ".DEFAULT_NAME_SPACE";
		return getInstance(namespace, expirationMilliseconds);
	}

	public static CacheInterceptor getInstance(String namespace, int expirationMilliseconds) {
		return getInstance(namespace, (long) expirationMilliseconds);
	}

	public static synchronized CacheInterceptor getInstance(String namespace, long expirationMilliseconds) {
		if (cacheInterceptors.containsKey(namespace) == true) {
			return cacheInterceptors.get(namespace);
		}
		CacheInterceptor ret = new CacheInterceptor(expirationMilliseconds);
		cacheInterceptors.put(namespace, ret);
		return ret;
	}

	public Object invoke(MethodInvocation invocation) throws Throwable {
		Method method = invocation.getMethod();
		Object syncObj = invocation.getThis();
		if (syncObj == null) {
			syncObj = method;
		}
		synchronized (syncObj) {
			CallDesc callDesc = new CallDesc(invocation);
			if (cacheMap.containsKey(callDesc) == true) {
				CacheData cacheData = cacheMap.get(callDesc);
				if (cacheData.expirationTime == -1) {
					return cacheData.data;
				} else if (System.currentTimeMillis() <= cacheData.expirationTime) {
					return cacheData.data;
				}
			}
			Object ret = invocation.proceed();
			if (ret != null) {
				CacheData cacheData = new CacheData();
				cacheData.data = ret;
				if (this.expirationMilliseconds < 0) {
					cacheData.expirationTime = -1;
				} else {
					cacheData.expirationTime = System.currentTimeMillis() + expirationMilliseconds;
				}
				cacheMap.put(callDesc, cacheData);
			}
			return ret;
		}
	}

	public static class CacheData {
		public long expirationTime;
		public Object data;
	}

	public static class CallDesc {
		private Method method;
		private Object target;
		private Object[] arguments;

		public CallDesc(MethodInvocation invocation) {
			method = invocation.getMethod();
			target = invocation.getThis();
			arguments = invocation.getArguments();
		}

		@Override
		public boolean equals(Object obj) {
			if (obj == null) {
				return false;
			}
			if (obj instanceof CallDesc == false) {
				return false;
			}
			CallDesc callDesc = (CallDesc) obj;
			if (method.equals(callDesc.method) == false) {
				return false;
			}
			if (equals(this.target, callDesc.target) == false) {
				return false;
			}
			if (equals(arguments, callDesc.arguments) == false) {
				return false;
			}

			return true;
		}

		private boolean equals(Object[] arguments1, Object[] arguments2) {
			if (arguments1 == null && arguments2 == null) {
				return true;
			}
			if (arguments1 == null && arguments2 != null) {
				return false;
			}
			if (arguments1 != null && arguments2 == null) {
				return false;
			}
			if (arguments1.length != arguments2.length) {
				return false;
			}
			for (int i = 0; i < arguments1.length; i++) {
				Object arg1 = arguments1[i];
				Object arg2 = arguments2[i];
				if (equals(arg1, arg2) == false) {
					return false;
				}

			}
			return true;
		}

		private boolean equals(Object arg1, Object arg2) {
			if (arg1 == null && arg2 == null) {
				return true;
			}
			if (arg1 == arg2) {
				return true;
			}
			if (arg1 != null) {
				return arg1.equals(arg2);
			} else {
				return arg2.equals(arg1);
			}
		}

		@Override
		public int hashCode() {
			return method.hashCode();
		}
	}
}
